from balldontlie import BalldontlieAPI
import os
import time
import re

# 每页最大条数
PER_PAGE = 100
# 输出文件名（写入当前工作目录）
OUT_FILENAME = '数据2.txt'

api = BalldontlieAPI(api_key="your_apikey")

def extract_data_list(players_obj):
    """
    从不同返回结构中提取 data 列表：
    - 如果是 dict 且包含 'data'，返回 players_obj['data']
    - 否则尝试取 players_obj.data（对象形式）
    - 最后如果就是列表则直接返回
    """
    if isinstance(players_obj, dict) and 'data' in players_obj:
        return players_obj['data']
    data = getattr(players_obj, 'data', None)
    if data is not None:
        return data
    # 假设本身就是列表
    return players_obj

def get_meta_next_cursor(players_obj):
    """
    尝试从返回值中读取 meta.next_cursor（支持 dict.meta 或 对象.meta）
    返回 None 或 数值（int）
    """
    if isinstance(players_obj, dict):
        meta = players_obj.get('meta') or {}
        return meta.get('next_cursor')
    meta = getattr(players_obj, 'meta', None)
    if meta is None:
        return None
    # meta 可能是对象或 dict
    return getattr(meta, 'next_cursor', None) if not isinstance(meta, dict) else meta.get('next_cursor')

def format_names_from_batch(batch):
    """
    接受一批球员条目（每项可能是 dict 或 对象），返回按格式化好的名单行列表：
    每项形如: "\"First Last\","
    """
    lines = []
    for p in batch:
        if isinstance(p, dict):
            first = p.get('first_name') or p.get('first')
            last = p.get('last_name') or p.get('last')
        else:
            first = getattr(p, 'first_name', None)
            last = getattr(p, 'last_name', None)
        if first and last:
            # 确保使用英文双引号与英文逗号
            lines.append(f'"{first} {last}",')
    return lines

def append_lines_to_file(lines, out_path):
    """以追加模式写入行列表（自动换行）"""
    if not lines:
        return
    with open(out_path, 'a', encoding='utf-8') as f:
        f.write('\n'.join(lines) + '\n')

def count_valid_lines_in_file(path):
    """
    统计文件中有效行数（符合 "First Last", 格式的行）
    返回整数
    """
    if not os.path.exists(path):
        return 0
    pattern = re.compile(r'^".+",\s*$')
    valid = 0
    with open(path, 'r', encoding='utf-8') as f:
        for line in f:
            if pattern.match(line.strip()):
                valid += 1
    return valid

def run_fetch_all():
    """
    1) 程序开始时尝试读取指定的 txt 文档：
       - 若不存在，创建并从 cursor=0 开始
       - 若存在，读取有效行数 n，并将 cursor 设置为 max(0, n-1)
    2) 使用 cursor 参数循环分页请求（per_page=PER_PAGE）
       - 每次请求后检查返回的 batch 长度
       - 读取返回 JSON 的 meta.next_cursor（若可用）
       - 验证 meta.next_cursor 是否与预测值（cursor + len(batch)）一致；
         若不一致，则使用 meta.next_cursor 作为下次的 cursor（以避免缺失）
    3) 遇到限流（包含 429 或 'Too Many Requests' 或 'rate' 的异常消息）则等待 60 秒后重试当前请求
    """
    out_path = os.path.join(os.getcwd(), OUT_FILENAME)

    # 确保文件存在
    if not os.path.exists(out_path):
        # 创建空文件，从 0 开始
        open(out_path, 'w', encoding='utf-8').close()
        cursor = 0
        print(f'输出文件不存在，已创建：{out_path}，从 cursor=0 开始抓取。')
    else:
        valid_lines = count_valid_lines_in_file(out_path)
        # 按用户要求，cursor 设置为 有效行数
        cursor = max(0, valid_lines)
        print(f'检测到已有输出文件，包含 {valid_lines} 条有效行，设置起始 cursor={cursor}。')

    total_written = 0
    last_cursor = None
    consecutive_no_progress = 0

    while True:
        try:
            players = api.nba.players.list(per_page=PER_PAGE, cursor=cursor)
        except Exception as e:
            msg = str(e)
            # 如果是限流响应（429 / Too Many Requests / rate），等待 60 秒后重试；否则抛出异常停止程序
            if '429' in msg or 'Too Many Requests' in msg or 'rate' in msg.lower():
                print(f'收到限流响应（{msg}），等待 60 秒后重试（cursor={cursor}）...')
                time.sleep(60)
                continue
            raise RuntimeError(f'API 请求失败（cursor={cursor}）：{msg}') from e

        batch = extract_data_list(players) or []
        batch_len = len(batch)

        # 获取 meta.next_cursor（如果提供）
        meta_next = get_meta_next_cursor(players)

        # 计算预测下一个 cursor
        predicted_next = cursor + batch_len

        # 验证 meta_next 是否与预测一致
        if meta_next is not None:
            try:
                meta_next_int = int(meta_next)
            except Exception:
                meta_next_int = meta_next
            if meta_next_int != predicted_next:
                print(f'注意：meta.next_cursor={meta_next_int} 与 预测值={predicted_next} 不一致。将使用 meta.next_cursor 以避免遗漏。')
                next_cursor = meta_next_int
            else:
                next_cursor = predicted_next
        else:
            # 如果没有提供 meta.next_cursor，则采用预测值
            next_cursor = predicted_next

        # 如果本批没有数据，抛出异常并停止程序（按要求）
        if batch_len == 0:
            raise RuntimeError(f'API 返回无数据（cursor={cursor}），程序停止。')

        # 将本批格式化并写入文件（追加）
        lines = format_names_from_batch(batch)
        append_lines_to_file(lines, out_path)

        written = len(lines)
        total_written += written
        print(f'cursor={cursor}，本次返回 {batch_len} 条，写入 {written} 条，总计写入 {total_written} 条。')

        # 检测是否无进展以避免无限循环
        if last_cursor is not None and next_cursor == last_cursor:
            consecutive_no_progress += 1
        else:
            consecutive_no_progress = 0

        if consecutive_no_progress >= 3:
            print('检测到连续多次 cursor 未推进，终止以避免死循环。')
            break

        last_cursor = cursor
        # 更新 cursor 为 next_cursor（确保为非负整数）
        try:
            cursor = int(next_cursor)
            if cursor < 0:
                print('计算出的下次 cursor 为负值，结束抓取。')
                break
        except Exception:
            # 无法解析为 int，则结束
            print('无法解析下次 cursor，结束抓取以防止异常循环。')
            break

        # 若 meta_next 为 0 或 None 且 batch_len < PER_PAGE，则可能到达末尾
        if (meta_next in (0, None) or cursor == 0) and batch_len < PER_PAGE:
            print('检测到可能已到数据末尾，抓取结束。')
            break

        # 小睡以避免过快请求（可根据需要调整）
        time.sleep(0.2)

    print(f'完成抓取并写入，共新增 {total_written} 条，文件：{out_path}')

if __name__ == '__main__':
    run_fetch_all()
